
閱讀本篇文章前,仔細想想看
筆者列出到目前為止我們學到跟類別有關的名詞,可以回憶一下它們各自的定義以及實用的地方在哪裡~
- 類別與物件的差別 Class v.s. Object
- 成員變數與方法 Member Variables & Member Methods
- 存取修飾子與模式 Access Modifiers(
public/private/protected)- 建構子函式 Constructor Function
- 類別繼承 Inheritance 與
super關鍵字- 靜態屬性與方法 Static Properties & Methods
- 存取方法 Accessors (Getter Methods & Setter Methods)
如果還沒理解完畢的話,可以先翻看 Day 18. ~ Day 22. 的文章喔!
另外,本篇所舉的例子會承接 Day 26. 策略模式篇章進行下去,不過也會前情提要一下,所以讀者放心!
其實筆者壓根沒想到關於類別的主題會寫這麼多,不過既然是一系列完整的教學文,筆者認為有必要好好的把任何細節交代清楚。
另外,本篇章的範例程式碼已經放在 Maxwell-Alexius/Iron-Man-Competition 這個 Repo.。
因此本日正文開始囉~
本篇文章的範例承接 Day 26. —— 運用策略模式設計陽春版 RPG 遊戲角色機制,那時候談到的東西是如何搭配介面(Interface)與類別(Class),結合起來並應用策略模式(Strategy Pattern)寫出容易管理、可以重複使用的程式碼。
不過本篇運用的起始程式碼跟 Day 26. 的結果ㄧ樣 —— 就是實踐到可以切換攻擊策略的功能,但還沒實踐裝備武器的功能。以下就是今天的起始範例程式碼,筆者就先快速說明帶過,讀者也是能夠理解就可以趕快進入本篇章的下個重點。
首先是主要的父類別 Character 的程式碼。

name 代表角色名稱role 代表角色職業,為列舉型別,分別有:Role.Swordsman、Role.Warlock、Role.Highwayman 以及 Role.BountyHunter 四種職業attackRef 代表攻擊策略 Attack 的參考點,主要是策略模式中最重要的父類別與策略的連結introduce 為簡單的角色自我介紹方法attack 方法則是藉策略模式 —— 由 attackRef 連結到的攻擊策略 —— 代為執行角色攻擊的程序switchAttackStrategy 則是負責將 Attack 策略進行切換的動作攻擊的策略介面很簡單,就只有 attack 方法需要實現而已,程式碼如下。

另外,根據 Attack 介面延伸出三種不同的攻擊方式 —— MeleeAttack、MagicAttack 以及 StabAttack,其中筆者就貼 MeleeAttack 的程式碼,因為其他兩種攻擊策略大致上的實踐方式都差不多。

最後,就是根據 Character 繼承過後,子類別的實踐 —— 也就是角色被建立的邏輯,以下以 Swordsman 的程式碼為例,而另一個職業 Warlock 的實踐方式也是大同小異呢。

好,我們今天就快速進入正題。
筆者按照本系列的調性:先遇到問題,才開始進行正題的討論。
首先,今天要實作的東西跟昨天舉的案例一模ㄧ樣 —— 就是實踐遊戲角色裝備武器 Weapon 的功能。
讀者云:“搞什麼啊!作者是在故意耍人嗎!?難道以為可以寫ㄧ樣的內容草草帶過嗎?”
不!不!不!(請不要丟爛番茄)
筆者的意思是:儘管今天跟昨天要達成的需求ㄧ樣,但示範的實作方式不同,這也是要彰顯設計系統的彈性 —— 你不需要更多進階的技巧,例如裝飾子 Decorators、泛用型別 Generics等 —— 光是學會正確地使用介面與類別就可以寫出很不錯的應用,這應該才是厲害的地方。(進階的東西會在第四篇章以後介紹,現在還在第二篇章)
如果筆者只有帶過語法但沒有講些應用的話,就算介紹進階功能,讀者不會用 —— 學了 TypeScript 根本就沒 P 用,回去用原生 JS 還比較自由些,本身又可以變出很棒又很蠢的戲法。
今天筆者希望達到這個目標:
與其讓角色能夠直接切換攻擊策略,不如藉由裝備武器
Weapon—— 進行攻擊策略的切換與使用。
前一篇著重的點是:Character 同時有 attackRef 連結攻擊策略、weaponRef 連結武器的選擇 —— 藉由切換武器的同時,更新攻擊策略。
今天的目標則是:Character 只會有 weaponRef 連結裝備的武器;而裝備的武器 Weapon 有 attackRef 的設定,藉由 weaponRef 進行呼叫攻擊的動作。所以本篇文和前一篇文實作是有差別的喔!
讀者剛開始可能會覺得模糊,不過筆者一步步示範給讀者看 —— 按照前兩篇策略模式四步驟嚴格執行。(所以這是第三次示範策略模式了 XD)
首先,第一件事情就是先規範好武器 Weapon 的介面,畢竟武器的選擇也可以被策略模式給應用。

武器的介面有以下這些性質:
name 代表武器名稱,為唯讀模式availableRoles 代表可以被裝備該武器的職業attackStrategy 代表武器跟 Attack 攻擊策略之間的參考點(reference point)switchAttackStrategy 函式負責進行攻擊策略的切換attack 方法就是負責實現角色攻擊的功能 —— 由於是策略模式,所以會藉由 attackStrategy 進行呼叫筆者照樣實踐三種不同的武器策略:BasicSword、BasicWand 與 Dagger。



以上就是對在 Weapon 介面下,延伸出來三種不同的武器策略。
貼心小提示
敏銳的讀者一定發現:三種武器策略的
switchAttackStrategy與attack成員方法重複了 —— 因此違反了 DRY(Don't Repeat Yourself)原則!筆者這邊要恭喜讀者:能夠注意到這個點,就代表讀者快抓到 —— 判斷使用抽象類別的時機點的感覺!
不過這裡要請讀者繼續看下去~
這個步驟應該對讀者來說算單純 —— 但是要注意,本日目標明確指定一點:Character 類別必須藉由裝備的武器 Weapon 進行角色攻擊的動作。
以下就是對 Character 類別連結 Weapon 的實踐:

其中,筆者建立了 weaponRef 負責連結 Character 與武器之間的關係 —— 儘管就只有一行宣告而已,但卻是使用策略模式的重要關鍵呢!
接下來就是要讓角色的武器能夠攻擊別人。以下是對 Character 類別的實作過程:

Character 的 attack 成員方法是藉由 weaponRef 呼叫它的 attack 方法,將角色與被攻擊的角色傳遞下去,直到發動攻擊的策略。(這感覺跟英文單字 —— propagation 的行為很像)
另外,equip 方法則是負責切換角色的武器選擇(武器策略) —— 也會根據武器的 availableRoles 進行檢測,判斷該武器是否能夠被該角色裝備。
最後,我們在 Swordsman 與 Warlock 這兩個類別進行武器策略的初始化:


以下是簡單的程式碼檢驗。(編譯並且執行結果如圖一)


圖一:我們成功地讓武器可以被切換,照常可以動作!
另外,除了武器可以被切換外,我們也可以建立 BasicSword 物件並且將其 Attack 連結的策略從原本預設的 MeleeAttack 切換成 StabAttack。
以下的程式碼檢測結果如圖二。


圖二:將 BasicSword 的攻擊策略切換為 StabAttack 也可以生效!
相信讀者看到這裡,會覺得策略模式還蠻好用的。從這裡開始,筆者要解決這個問題 —— 每次實踐新的武器,都會出現的重複的程式碼如下:

回憶過往本系列學到的東西:好像可以將那些重複的方法實踐整理起來,放在父類別,再一併繼承下去。
於是筆者將**Weapon 從介面晉升為類別等級**,並且把 switchAttackStrategy 跟 attack 成員方法的實踐寫下去。
不過這裡又會出現問題:name、availableRole 與 attackStrategy 這些東西在父類別是不確定的,必須強制讓子類別去進行覆蓋的動作 —— 一種解法是,父類別針對這些屬性進行預設值的動作,於是出來的 Weapon 類別的實踐結果如下:

由於 Weapon 從介面晉升為類別,所有 Weapon 延伸出的武器策略必須從 implements 改成 extends —— 也就是類別的繼承。以下就是 BasicSword、BasicWand 與 Dagger 實踐過後的結果(基本上長得都差不多):



有些讀者認為這樣就夠了,但筆者可不這麼認同,因為父類別 Weapon 的實踐失去了介面的彈性,我們只能用預設值的方式防止程式碼壞掉,但不能利用介面的技巧,強迫子類別實踐出 name、availableRoles 與attackStrategy 等成員。
如果同時想要擁有:
- 類別的性質 —— 成員有實際的實踐過程,以及
- 介面的性質 —— 一但跟介面簽訂條約,就必須強制實踐介面指名的功能
則可以選擇使用抽象類別(Abstract Class)!
要運用抽象類別很簡單,宣告抽象類別時記得使用 abstract class 關鍵字,並且在該抽象類別的成員裡,可以選擇:
abstract
因此,將 Weapon 從類別再轉換成抽象類別,程式碼會變得更簡潔呢!

你可以發現:name、availableRoles 與 attackStrategy 被註記為 abstract,代表子類別若沒有實踐這些功能,就會被 TypeScript 警告。(錯誤訊息如圖三)

圖三:筆者刻意在 Weapon 的子類別 —— BasicSword 裡面,將 name 欄位砍掉,結果被 TypeScript 警告,因為 name 是父類別的抽象成員,必須被實踐!
這裡筆者就略過程式碼檢驗的過程,讓讀者自己去嘗試看看吧!
重點 1. 抽象類別的宣告與意義 Abstract Class
介面與類別各自的特點,分別如下:
- 介面的特點:一但跟介面進行綁定的動作,TypeScript 會針對沒有被實踐到的規格進行監控的動作
- 類別的特點:定義物件的完整藍圖與實踐過程
如果想要兼顧介面與類別的優勢 —— 繼承父類別的同時,也能夠彈性地宣告規格,而非直接實踐出過程,則可以選擇使用抽象類別(Abstract Class)。
若抽象類別
AbstractC的宣告方式如下:
則一但繼承
AbstractC的子類別擁有以下特性與條件:
- 繼承了
AbstractC的成員變數Prop與成員方法Method- 必須實踐成員變數
Pabstract以及成員方法Mabstract
另外,抽象類別也會有些限制 —— 可以藉由推理就推出特性:
重點 2. 抽象類別的限制 Limitation of Abstract Class
- 抽象類別不能進行建立物件的動作:因為裡面的抽象成員是還未實踐的狀態,就算硬要從抽象類別建立物件,該物件也會是不完整狀態
- 根據前一點推斷:抽象類別生來就是要被繼承的
- 抽象類別裡的抽象成員(Abstract Member),由於要滿足介面的特性 —— 代表規格並且強迫繼承的子類別必須實踐功能,因此抽象成員必需被實踐為
public模式
重點 2 提到的最後一點,抽象成員必為 public 模式跟類別實踐介面本身的規格,那些成員必須為 public 模式的邏輯是一模一樣的!
筆者總算把 TypeScript 類別的最後一部分的語法交代完畢~
下一篇筆者要介紹抽象工廠模式這個設計模式~算是介面和類別結合的延伸應用喔~!
看完文章後有個問題:
由於抽象類別的成員必須被實踐為 public 模式,以本程式碼來說,創建 Weapon 類別的物件後,可以直接存取 attackStrategy
並直接改變 attackStrategy,略過 switchAttackStrategy 的邏輯,造成武器與攻擊 mapping 上的錯誤。
此類問題有建議的解決方法嗎?
我好像有解法了XD
實作抽象類別的類別本身實作抽象成員時設 readonly,就無法透過該類別建立的物件直接修改 attackStrategy
而 switchAttackStrategy 還是能修改 attackStrategy 的原因是,switchAttackStrategy 是在抽象類別內定義,
抽象類別本身定義的抽象成員沒有設 readonly,所以抽象類別本身的 switchAttackStrategy 可以修改 attackStrategy
嗨~看完這部分後,有幾個疑惑想詢問一下:
為何不用 super(),而要改用 abstract 方法,效果有什麼差別嗎?
Character.ts 的部分,是不是也適合改成 abstract class?
謝謝。
另外 abstract 應該不只能用 public 也是可以用 protected 的。